/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */

package au.com.lastweekend.ws2300;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.com.lastweekend.ws2300.proxy.WS2300Proxy;

/**
 * This class performs the client side implementation of the WS2300 serial protocol to raw IO streams.
 * 
 * @see WS2300Proxy for the device side protocol handling.
 * @author ggardner
 * 
 */
public class RawDevice extends Device {

    private static final Logger LOG = LoggerFactory.getLogger(RawDevice.class);

    /**
     * All commands fit this mask 00 00 00 10 .
     */
    public static final int COMMAND_ALL = 0x02;

    /**
     * Read 11 nn nn 10 where nnnn is the number of BYTES to read
     */
    public static final int COMMAND_READ = 0xC2;
    public static final int ACK_READ = 0x30;

    /**
     * Address 10 aa aa 10 aaaa is the address
     */
    public static final int COMMAND_ADDRESS = 0x82;

    /**
     * Write 01 vv vv 10 vvvv is the value to write at the current address
     */
    public static final int COMMAND_WRITE = 0x42;
    public static final int ACK_WRITE = 0x10;

    /**
     * Set bit 00 11 bb 10 bb is bit address to unset
     */
    public static final int COMMAND_UNSET_BIT = 0x32;
    public static final int ACK_UNSET_BIT = 0x0C;

    /**
     * Set bit 00 01 bb 10 bb is bit address to set
     */
    public static final int COMMAND_SET_BIT = 0x12;
    public static final int ACK_SET_BIT = 0x04;

    /**
     * Reset 00 00 01 10 reset
     */
    public static final int COMMAND_RESET = 0x06;
    public static final int ACK_RESET = 0x02;

    /**
     * The protocol only handles reading a certain number of bytes at a time. Subclasses should split reads up into blocks of
     * MAX_READ_BYTES
     */
    protected static final int MAX_READ_BYTES = 15;

    private InputStream inputStream;
    private OutputStream outputStream;

    private boolean resetRequired = true;

    public RawDevice(InputStream in, OutputStream out) {

        setInputStream(in);
        setOutputStream(out);
    }

    /**
     * Utility constructor for bean creation methods. Must call {@link #setInputStream(InputStream)} and
     * {@link #setOutputStream(OutputStream)} before using for reading/writing
     */
    public RawDevice() {

    }

    private void sendAddress(int address) throws IOException {

        if (address < 0 || address > Device.MAX_ADDRESS) {
            throw new IllegalArgumentException(Integer.toHexString(address) + " is out of address range 0 - "
                    + Integer.toHexString(Device.MAX_ADDRESS));
        }

        // First 4 bytes are populated with converted address range 0000-13B0
        for (int i = 0; i < 4; i++) {
            int nibble = (address >> (4 * (3 - i))) & 0x0F;
            int addressByte = ((COMMAND_ADDRESS + (nibble << 2)) & 0xFF);
            int checkSum = (i << 4) + nibble;
            sendCommand(addressByte, checkSum);
        }
    }

    private void sendCommand(int command, int check) throws IOException {

        outputStream.write(command);
        outputStream.flush();
        readCheck(check);
    }

    private void readCheck(int check) throws IOException, EOFException {

        int response = inputStream.read();
        if (response < 0) {
            throw new EOFException("Unexpected EOF (" + response + ")");
        }
        if (response != check) {
            resetRequired = true;
            throw new IOException("Unexpected response " + Integer.toHexString(response) + ", expected "
                    + Integer.toHexString(check));
        }

    }

    private void reset() throws IOException {

        if (!resetRequired) {
            return;
        }

        int available = inputStream.available();

        if (available > 0) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Skipping " + available + " bytes");
            }
            inputStream.skip(available);
        }

        sendCommand(COMMAND_RESET, ACK_RESET);
        resetRequired = false;
        return;
    }

    public synchronized byte[] read(int address, int numBytes) throws IOException {

        reset();

        if (numBytes > MAX_READ_BYTES || numBytes < 1) {
            throw new IllegalArgumentException("Read number " + numBytes + "is out of range 0 -" + MAX_READ_BYTES);
        }

        if (address < 0 || address + (numBytes / 2) > MAX_ADDRESS) {
            throw new IllegalArgumentException(String.format("Read %d bytes(!) from %04x - address out of range 0 - %04x",
                    numBytes, address, MAX_ADDRESS));
        }

        byte[] resultBytes = new byte[numBytes];

        sendAddress(address);

        // Command to request a fixed number of results
        int readBytesCommand = COMMAND_READ + (numBytes << 2);
        int readBytesChecksum = ACK_READ + numBytes;
        sendCommand(readBytesCommand, readBytesChecksum);

        // Read the data bytes
        int offset = 0;
        while (offset < numBytes) {
            int read = inputStream.read(resultBytes, offset, numBytes - offset);
            if (read < 0) {
                throw new IOException("Unexpected EOF (" + read + ") reading from input stream");
            }
            offset += read;
        }

        int dataCheck = 0;
        byte[] resultNibbles = new byte[numBytes * 2];

        for (int i = 0; i < numBytes; i++) {
            resultNibbles[2 * i] = (byte) (resultBytes[i] & 0x0F);
            resultNibbles[2 * i + 1] = (byte) ((resultBytes[i] >> 4) & 0x0F);

            dataCheck += resultBytes[i];
        }
        readCheck(dataCheck & 0xFF);

        return resultNibbles;
    }

    public synchronized void write(int address, byte[] nibbles) throws IOException {

        reset();

        int numNibbles = nibbles.length;

        if (address < 0 || address + numNibbles > MAX_ADDRESS) {
            throw new IllegalArgumentException(String.format("write(%04x,%d) address out of range 0 - %04x", address, numNibbles,
                    MAX_ADDRESS));
        }

        sendAddress(address);

        for (int i = 0; i < numNibbles; i++) {
            // data_out[i] = (unsigned char) (encode_constant + (data_in[i] * 4));
            byte nibbleValue = (byte) (nibbles[i] & 0x0F);
            int dataCommand = COMMAND_WRITE + (nibbleValue << 2);
            int dataCheck = ACK_WRITE + nibbleValue;
            sendCommand(dataCommand, dataCheck);
        }

    }

    public synchronized void writeBit(int nibbleAddress, int bitAddress, boolean setBit) throws IOException {

        reset();

        if (bitAddress < 0 || bitAddress > 3) {
            throw new IllegalArgumentException("Bit address " + bitAddress + " is out of range 0 - 3");
        }

        sendAddress(nibbleAddress);

        int bitCommand = (setBit ? COMMAND_SET_BIT : COMMAND_UNSET_BIT) + (bitAddress << 2);
        int bitCheck = (setBit ? ACK_SET_BIT : ACK_UNSET_BIT) + bitAddress;

        sendCommand(bitCommand, bitCheck);
    }

    public InputStream getInputStream() {

        return inputStream;
    }

    /**
     * 
     * @param inputStream
     */
    public void setInputStream(InputStream inputStream) {

        this.inputStream = inputStream;
    }

    public OutputStream getOutputStream() {

        return outputStream;
    }

    /**
     * 
     * @param outputStream
     */
    public void setOutputStream(OutputStream outputStream) {

        this.outputStream = outputStream;
    }

}